﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;
using System.Reflection;
using System.ComponentModel;
using Novell.Directory.Ldap;
using System.Globalization;

namespace Novell.DirectoryServices.Linq
{
    internal class LdapObjectReader<T> : IEnumerable<T>, IEnumerable //where T : class, new()
    {
        LdapSearchResults _results;
        Delegate _project;
        Type _originalType;
        IDirectorySource _source;
        public LdapObjectReader(IDirectorySource source, LdapSearchResults entries, Delegate project, Type originalType)
        {
            this._source = source;
            this._originalType = originalType;
            this._project = project;
            this._results = entries;
        }
        public IEnumerator<T> GetEnumerator()
        {
            DirectorySchemaAttribute[] attr = (DirectorySchemaAttribute[])_originalType.GetCustomAttributes(typeof(DirectorySchemaAttribute), false);
            if (attr == null || attr.Length == 0)
                throw new InvalidOperationException("Missing schema mapping attribute.");

            while (_results.hasMore())
            {
                LdapEntry e = _results.next();

                object result = Activator.CreateInstance(_project == null ? typeof(T) : _originalType);

                DirectoryEntity entity = result as DirectoryEntity;

                if (_project == null)
                {
                    foreach (PropertyInfo p in typeof(T).GetProperties())
                        AssignResultProperty(e, result, p.Name);

                    if (entity != null)
                        InitializeNewEntry(e, entity);

                    yield return (T)result;
                }
                else
                {
                    foreach (PropertyInfo prop in _originalType.GetProperties())
                        AssignResultProperty(e, result, prop.Name);

                    if (entity != null)
                        InitializeNewEntry(e, entity);
                    yield return (T)_project.DynamicInvoke(result);
                }


            }
        }

        private void InitializeNewEntry(LdapEntry e, DirectoryEntity entity)
        {
            if (_source is IUpdatableDirectorySource)
            {
                entity.InitializeInternal((IUpdatableDirectorySource)this._source, e);
            }
            else
            {
                entity.InitializeInternal(this._source, e);
            }
        }
        IEnumerator IEnumerable.GetEnumerator()
        {
            return this.GetEnumerator();
        }
        /// <summary>
        /// Assigns the specified property on the specified object based on the data wrapped in the DirectoryEntry representing the current result.
        /// </summary>
        /// <param name="helper">Active Directory helper type to help retrieving the target value.</param>
        /// <param name="searchResult">SearchResult object containing the data for the current result.</param>
        /// <param name="result">Object the property has to be set on.</param>
        /// <param name="prop">Property to be set.</param>
        private void AssignResultProperty(LdapEntry searchResult, object result, string prop)
        {
            PropertyInfo i = _originalType.GetProperty(prop);
            DirectoryAttributeAttribute[] da = i.GetCustomAttributes(typeof(DirectoryAttributeAttribute), false) as DirectoryAttributeAttribute[];
            if (da != null && da.Length != 0)
            {
                LdapAttribute attribute = searchResult.getAttribute(da[0].Attribute);
                //The attribute isn't defined so just silently return.
                if (attribute == null)
                {
                    return;
                }
                //var resultValue = e.getAttribute(da[0].Attribute).;
                if (i.PropertyType.IsArray)
                {
                    //
                    // Byte array properties are special in AD. Here we don't follow the
                    // heuristic of an array-typed property to be an expanded multi-value
                    // property but we support splatting the contents of the value into
                    // the byte[] array.
                    //

                    //TODO: Create a class to handle these conversions.
                    //Convert the sbyte[] to a byte[]
                    byte[] value = null;
                    if(attribute.ByteValue != null)
                    {
                        value = new byte[attribute.ByteValue.Length];
                        Buffer.BlockCopy(attribute.ByteValue,0,value,0,attribute.ByteValue.Length);
                    }
                    
                    if (i.PropertyType.GetElementType() == typeof(byte) && value != null)
                    {
                        //Use the byte[]
                        i.SetValue(result, value, null);
                    }
                    else
                    {
                        //Use a regular array.
                        //Use a string of values.
                        Array o = Array.CreateInstance(i.PropertyType.GetElementType(), attribute.StringValueArray.Length);

                        int j = 0;
                        foreach (string oo in attribute.StringValueArray)
                            o.SetValue(oo, j++);

                        i.SetValue(result, o, null);
                    }
                }
                else
                {
                    if (attribute.StringValueArray.Length == 1)
                    {
                        //
                        // Support GUID field mapping.
                        //

                        if (i.PropertyType == typeof(Guid))
                        {
                            if (attribute.ByteValue == null || attribute.ByteValue.Length == 0)
                                throw new NotSupportedException("Mapping of Guid-typed property " + i.Name + " to non-byte[] valued directory field " + da[0].Attribute + ".");


                            byte[] value = new byte[attribute.ByteValue.Length];
                            Buffer.BlockCopy(attribute.ByteValue, 0, value, 0, attribute.ByteValue.Length);
                            i.SetValue(result, new Guid(value), null);
                        }
                        else if (i.PropertyType.IsEnum)
                        {
                            object value = attribute.StringValue;
                            if (value != null)
                            {
                                try
                                {
                                    value = Enum.Parse(i.PropertyType, (string)value, true);
                                }
                                catch (ArgumentException arg)
                                {
                                    throw new ArgumentException("Cannot assign value " + value.ToString() + " to a property of type " + i.PropertyType.ToString(), arg);
                                }
                                i.SetValue(result, value, null);

                            }
                        }
                        else
                        {
                            //Need to determine if the type can be changed
                            //TODO: Create a test harness to detect if the two types aren't compatible.
                            i.SetValue(result, Convert.ChangeType(attribute.StringValue, i.PropertyType,CultureInfo.InvariantCulture), null);
                        }
                    }
                    
                }
            }
        }
    }
}
